using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Rifts.Models.Characters;
using System.Diagnostics.Contracts;
using Granite.DataAnnotations;

namespace Rifts.Models.Definitions
{
	public class SkillDefinition
	{
		readonly int? m_FreeSkillLevel;
		readonly Catalog m_Catalog;
		readonly string m_GroupName;
		readonly XElement m_BookDefinition;
		readonly XElement m_SkillDefintion;
		readonly int? m_OverrideBaseScore;
		readonly int? m_OverridePerLevel;
		readonly int? m_OverrideSecondarySkill;

		public SkillDefinition(Catalog catalog, XElement bookDefinition, string groupName, XElement skillDefintion)
		{
			m_Catalog = catalog;
			m_BookDefinition = bookDefinition;
			m_GroupName = groupName;
			m_SkillDefintion = skillDefintion;

			foreach (var subSkill in skillDefintion.Elements("SubSkill"))
			{
				FreeSkills.Add(new SkillDefinition(catalog, bookDefinition, groupName, subSkill));
			}

			Verify();
		}

		/// <summary>
		/// This copies a skill into a different skill group.
		/// </summary>
		/// <param name="groupName"></param>
		/// <param name="skillToCopy"></param>
		public SkillDefinition(Catalog catalog, string groupName, SkillDefinition skillToCopy)
		{
			m_Catalog = catalog;
			m_GroupName = groupName;
			m_BookDefinition = skillToCopy.m_BookDefinition;
			m_SkillDefintion = skillToCopy.m_SkillDefintion;

			foreach (var subSkill in skillToCopy.m_SkillDefintion.Elements("Skill"))
			{
				FreeSkills.Add(new SkillDefinition(catalog, skillToCopy.m_BookDefinition, groupName, subSkill));
			}

			Verify();
		}

		/// <summary>
		/// This is used for free skills granted by another skill
		/// </summary>
		/// <param name="groupName"></param>
		/// <param name="skillToCopy"></param>
		/// <param name="overrideBaseScore"></param>
		/// <param name="overrideSecondarySkill"></param>
		/// <param name="overridePerLevel"></param>
		public SkillDefinition(string groupName, SkillDefinition skillToCopy, int? overrideBaseScore, int? overrideSecondarySkill, int? overridePerLevel, int? freeSkillLevel)
		{
			m_OverrideSecondarySkill = overrideSecondarySkill;
			m_OverridePerLevel = overridePerLevel;
			m_OverrideBaseScore = overrideBaseScore;
			m_GroupName = groupName;
			m_BookDefinition = skillToCopy.m_BookDefinition;
			m_SkillDefintion = skillToCopy.m_SkillDefintion;
			m_FreeSkillLevel = freeSkillLevel ?? 1;

			Verify();
		}

		private void Verify()
		{
			var metaData = new Character();
			var meleeTest = new FightingStyle();
			var wpTest = new WeaponProficiency();

			if (m_SkillDefintion.Name == "SubSkill" && m_SkillDefintion.HasElements)
				throw new Exception(String.Format("The sub-skill {0} in book {1} is not allowed to have {2} nodes", Name, BookName, m_SkillDefintion.Elements().First().Name));

			foreach (var node in m_SkillDefintion.Elements())
			{
				switch (node.Name.LocalName)
				{
					case "Attribute":
						if (!node.HasAttribute("Name"))
							throw new Exception(string.Format("Missing name in {0} block for the skill {1} in book {2}", node.Name, Name, BookName));

						var attributeType = node.GetEnum<AttributeType>("Type", AttributeType.General);
						if (attributeType == AttributeType.General)
						{
							try
							{
								metaData.BumpAttribute(node.GetAttribute("Name"), 0);
							}
							catch (ArgumentOutOfRangeException ex)
							{
								throw new Exception(string.Format("The attribute named {0} in the skill {1} in book {2} does not exist.", node.GetAttribute("Name"), Name, BookName), ex);
							}
							catch (Exception ex)
							{
								throw new Exception(string.Format("The attribute named {0} in the skill {1} in book {2} cannot be affected.", node.GetAttribute("Name"), Name, BookName), ex);
							}
						}
						else if (attributeType == AttributeType.FightingStyle)
						{
							if (!m_SkillDefintion.GetBool("IsFightingStyle"))
								throw new Exception(string.Format("The fighting style attribute named {0} in the skill {1} in book {2} can only appear in Fighting Style skills.", node.GetAttribute("Name"), Name, BookName));


							try
							{
								meleeTest.BumpAttribute(node.GetAttribute("Name"), 0);
							}
							catch (ArgumentOutOfRangeException ex)
							{
								throw new Exception(string.Format("The combat attribute named {0} in the skill {1} in book {2} does not exist.", node.GetAttribute("Name"), Name, BookName), ex);
							}
							catch (Exception ex)
							{
								throw new Exception(string.Format("The combat attribute named {0} in the skill {1} in book {2} cannot be affected.", node.GetAttribute("Name"), Name, BookName), ex);
							}

						}
						else if (attributeType == AttributeType.WP)
						{
							if (!m_SkillDefintion.GetBool("IsWP"))
								throw new Exception(string.Format("The WP attribute named {0} in the skill {1} in book {2} can only appear in WP skills.", node.GetAttribute("Name"), Name, BookName));


							try
							{
								wpTest.BumpAttribute(node.GetAttribute("Name"), 0);
							}
							catch (ArgumentOutOfRangeException ex)
							{
								throw new Exception(string.Format("The WP attribute named {0} in the skill {1} in book {2} does not exist.", node.GetAttribute("Name"), Name, BookName), ex);
							}
							catch (Exception ex)
							{
								throw new Exception(string.Format("The WP attribute named {0} in the skill {1} in book {2} cannot be affected.", node.GetAttribute("Name"), Name, BookName), ex);
							}

						}
						else
						{
							throw new Exception("Unkown attribute type " + attributeType);
						}

						break;


					case "Ability":
						{
							if (!node.HasAttribute("Name"))
								throw new Exception(string.Format("Missing name in {0} block for the skill {1} in book {2}", node.Name, Name, BookName));

							//if (!node.HasAttribute("Type"))
							//	throw new Exception(string.Format("Missing type in ability name {0} for the skill {1} in book {2}", node.GetAttribute("Name"), Name, BookName));

							var abilityType = node.GetEnum<AbilityType>("Type", AbilityType.General);
							if (abilityType == AbilityType.General || abilityType == AbilityType.Skill || abilityType == AbilityType.Combat)
							{

							}
							else if (abilityType == AbilityType.FightingStyle)
							{
								if (!m_SkillDefintion.GetBool("IsFightingStyle"))
									throw new Exception(string.Format("The fighting style ability named {0} in the skill {1} in book {2} can only appear in Fighting Style skills.", node.GetAttribute("Name"), Name, BookName));

							}
							else if (abilityType == AbilityType.WP)
							{
								if (!m_SkillDefintion.GetBool("IsWP"))
									throw new Exception(string.Format("The WP attribute named {0} in the skill {1} in book {2} can only appear in WP skills.", node.GetAttribute("Name"), Name, BookName));

							}
							else
							{
								throw new Exception("Unkown ability type " + abilityType);
							}



							break;
						}

					case "Requires":
						if (!node.HasElements)
							throw new Exception(string.Format("Requires block in the skill {1} in book {2} has no options listed", node.Name, Name, BookName));

						foreach (var childNode in node.Elements())
						{
							switch (childNode.Name.LocalName)
							{
								case "Skill":
								case "ClassGroup":
									break;
								default:
									throw new Exception(string.Format("Requires block in the skill {1} in book {2} has unknown option type {3}", node.Name, Name, BookName, childNode.Name.LocalName));
							}
						}

						break;

					case "BonusFrom":
						if (!node.HasAttribute("Name"))
							throw new Exception(string.Format("Missing name in {0} block for the skill {1} in book {2}", node.Name, Name, BookName));

						break;

					case "BonusTo":
						if (!node.HasAttribute("Name"))
							throw new Exception(string.Format("Missing name in {0} block for the skill {1} in book {2}", node.Name, Name, BookName));

						break;

					case "UnarmedAttack":
						if (!node.HasAttribute("Name"))
							throw new Exception(string.Format("Missing name in {0} block for the skill {1} in book {2}", node.Name, Name, BookName));

						break;

					case "WPChart":
					case "FreeSkill":
					case "SubSkill":
						break;

					//TODO: Add support for AttributeBonus
					//TODO: Add support for MinAttribute

					default:
						throw new Exception(string.Format("Unexpected element {0} in the skill {1} in book {2}", node.Name, Name, BookName));
				}
			}
		}



		public string GroupName
		{
			get { return m_GroupName; }
		}

		public string Name
		{
			get { return m_SkillDefintion.GetAttribute("Name"); }
		}

		public int? BaseScore
		{
			get { return m_OverrideBaseScore ?? m_SkillDefintion.GetInt2("BaseScore"); }
		}

		public int? SecondaryScore
		{
			get { return m_OverrideSecondarySkill ?? m_SkillDefintion.GetInt2("SecondaryScore"); }
		}

		public int? PerLevel
		{
			get { return m_OverridePerLevel ?? m_SkillDefintion.GetInt2("PerLevel"); }
		}

		public int? FreeSkillLevel
		{
			get { return m_FreeSkillLevel; }
		}

		readonly SkillDefinitionCollection m_FreeSkills = new SkillDefinitionCollection();
		public SkillDefinitionCollection FreeSkills
		{
			get { return m_FreeSkills; }
		}

		public void ApplySkill(SkillType skillType, Random random, Character character, int? fixedScore, int? occBonus, int? perLevelOverride)
		{
			if (skillType == SkillType.Unknown)
				throw new ArgumentOutOfRangeException("skillType", skillType, "The skillType is unknown");
			if (random == null)
				throw new ArgumentNullException("random", "random is null.");
			if (character == null)
				throw new ArgumentNullException("character", "character is null.");
			Contract.EndContractBlock();

			var skill = new Skill();
			skill.Name = Name;
			skill.BaseScore = fixedScore ?? BaseScore;
			skill.SecondaryBaseScore = SecondaryScore;
			skill.OccBonus = occBonus;
			skill.PerLevel = perLevelOverride ?? PerLevel;
			skill.IsHandToHand = IsFightingStyle;
			skill.SkillType = skillType;
			skill.BookReference = BookReference;

			//Look for a lower-valued skill to remove
			var oldSkill = character.Skills.FindByName(Name);

			bool skipBonuses = false;
			if (oldSkill != null) //use the better of the new/old skill values
			{
				oldSkill.BaseScore = Math.Max(oldSkill.BaseScore ?? 0, skill.BaseScore ?? 0);
				oldSkill.OccBonus = Math.Max(oldSkill.OccBonus ?? 0, skill.OccBonus ?? 0);
				oldSkill.OccBonus = Math.Max(oldSkill.PerLevel ?? 0, skill.PerLevel ?? 0);
				//replacing the old skill should not re-add the bonuses
				skipBonuses = true;
			}
			else
				character.Skills.Add(skill); //The IQ bonus will be automatically added.

			ApplyFreeSkills(random, character, skill); //this may up the score, but it won't re-apply bonuses from free skills

			if (!skipBonuses) //you can't get the bonuses a second time
			{
				FightingStyle fightingStyle = null;
				WeaponProficiency proficiency = null;
				if (IsFightingStyle)
				{
					fightingStyle = new FightingStyle() { Name = skill.Name };
					character.FightingStyles.Add(fightingStyle);
				}
				else if (IsWP)
				{
					proficiency = new WeaponProficiency() { Name = skill.Name };
					character.Proficiencies.Add(proficiency);
				}

				ApplyAttributes(random, character, skill, fightingStyle, proficiency);
				ApplyAbilities(random, character, skill, fightingStyle, proficiency);
				ApplyUnarmedAttacks(character, skill);
				ApplyBonuses(random, character);

				character.RecalculateStyles();
			}
		}

		private void ApplyFreeSkills(Random random, Character character, Skill skill)
		{
			foreach (var item in FreeSkills)
			{
				if (item.FreeSkillLevel != skill.Level)
					continue;

				item.ApplySkill(skill.SkillType, random, character, null, null, null);
			}
		}

		public static void ApplyLevelUp(Random random, Character character, Catalog catalog)
		{
			//level up existing skills
			foreach (var skill in character.Skills)
			{
				skill.Level += 1;
			}

			//add free skills
			foreach (var skill in character.Skills)
			{

				var skillDefinition = catalog.Skills.FindByName(skill.Name);
				if (skillDefinition == null)
					continue; //this must be a subskill
				//throw new Exception("Cannot find skill named " + skill.Name);

				skillDefinition.ApplyFreeSkills(random, character, skill);
			}

			foreach (var skill in character.Skills)
			{
				var skillDefinition = catalog.Skills.FindByName(skill.Name);
				if (skillDefinition == null)
					continue; //this must be a subskill

				var style = character.FightingStyles.FindByName(skill.Name);
				var wp = character.Proficiencies.FindByName(skill.Name);

				skillDefinition.ApplyAttributes(random, character, skill, style, wp);
				skillDefinition.ApplyAbilities(random, character, skill, style, wp);
			}
		}

		void ApplyAttributes(Random random, Character character, Skill skill, FightingStyle fightingStyle, WeaponProficiency proficicency)
		{
			foreach (var item in m_SkillDefintion.Elements("Attribute"))
			{
				if (!item.GetInts("Level", 1).Contains(skill.Level))
					continue;

				var attributeName = item.GetAttribute("Name");

				IAttributeClass target;
				switch (item.GetEnum<AttributeType>("Type", AttributeType.General))
				{
					case AttributeType.General:
						target = character;
						break;
					case AttributeType.FightingStyle:
						target = fightingStyle;
						break;
					case AttributeType.WP:
						target = proficicency;
						break;
					default:
						throw new Exception("Unknown attribute type " + item.GetAttribute("Type"));

				}

				var value = item.GetAttribute("Value");

				if (!string.IsNullOrEmpty(value))
				{
					target.SetAttribute(attributeName, random.Parse(value));
				}

				var add = item.GetAttribute("Add");
				if (!string.IsNullOrEmpty(add))
					target.BumpAttribute(attributeName, random.Parse(add));

				var subtract = item.GetAttribute("Subtract");
				if (!string.IsNullOrEmpty(subtract))
					target.BumpAttribute(attributeName, -1 * random.Parse(subtract));
			}

		}

		void ApplyUnarmedAttacks(Character character, Skill skill)
		{
			foreach (var abilityNode in m_SkillDefintion.Elements("UnarmedAttack"))
			{
				if (abilityNode.GetInt("Level", 1) != skill.Level)
					continue;

				character.UnarmedAttacks.Add(UnarmedAttack.Parse(abilityNode));
			}
		}

		void ApplyAbilities(Random random, Character character, Skill skill, FightingStyle fightingStyle, WeaponProficiency proficicency)
		{
			foreach (var abilityNode in m_SkillDefintion.Elements("Ability"))
			{
				if (abilityNode.GetInts("Level", 1).Contains(skill.Level))
					continue;

				var ability = new Ability(abilityNode, random, "Skill: " + skill.Name);

				switch (ability.Type)
				{
					case AbilityType.General:
						character.Abilities.Add(ability);
						break;
					case AbilityType.Combat:
						character.CombatAbilities.Add(ability);
						break;
					case AbilityType.Skill:
						skill.Abilities.Add(ability);
						break;
					case AbilityType.WP:
						proficicency.Abilities.Add(ability);
						break;
					case AbilityType.FightingStyle:
						fightingStyle.Abilities.Add(ability);
						break;
				}

			}
		}

		void ApplyBonuses(Random random, Character character)
		{
			//bonuses to other skills from this one
			foreach (var item in m_SkillDefintion.Elements("BonusTo"))
			{
				var skillName = item.GetAttribute("Name");

				var add = item.GetAttribute("Add");
				if (!string.IsNullOrEmpty(add))
					character.BumpSkill(skillName, random.Parse(add));
			}

			//bonsues from other skills to this one
			foreach (var item in m_SkillDefintion.Elements("BonusFrom"))
			{
				var skillName = item.GetAttribute("Name");
				if (character.Skills.Contains(skillName))
				{
					var add = item.GetAttribute("Add");
					if (!string.IsNullOrEmpty(add))
						character.BumpSkill(Name, random.Parse(add));
				}
			}

			//loop through all the skills that are not this skill
			foreach (var skill in m_Catalog.Skills.Where(s => s.Name != Name && character.Skills.Contains(s.Name)))
			{

				//bonuses to this skill from other skills
				foreach (var item in m_SkillDefintion.Elements("BonusTo"))
				{
					var skillName = item.GetAttribute("Name");
					if (string.Equals(skillName, Name, StringComparison.InvariantCultureIgnoreCase))
					{
						var add = item.GetAttribute("Add");
						if (!string.IsNullOrEmpty(add))
							character.BumpSkill(Name, random.Parse(add));
					}
				}

				//bonuses to this skill from other skills
				foreach (var item in m_SkillDefintion.Elements("BonusFrom"))
				{
					var skillName = item.GetAttribute("Name");
					if (string.Equals(skillName, Name, StringComparison.InvariantCultureIgnoreCase))
					{
						var add = item.GetAttribute("Add");
						if (!string.IsNullOrEmpty(add))
							character.BumpSkill(skill.Name, random.Parse(add));
					}
				}
			}
		}

		public bool IsFightingStyle
		{
			get { return m_SkillDefintion.GetBool("IsFightingStyle", false); }
		}

		public bool IsWP
		{
			get { return m_SkillDefintion.GetBool("IsWP", false); }
		}

		internal bool MetRequirements(Character character)
		{
			foreach (var requiresBlock in m_SkillDefintion.Elements("Requires"))
			{
				var minCount = requiresBlock.GetInt2("MinCount") ?? requiresBlock.Elements().Count();
				var hasCount = 0;
				foreach (var skill in requiresBlock.Elements("Skill"))
				{
					var skillName = skill.GetAttribute("Name");
					hasCount += character.Skills.FindAllByName(skillName).Count();
				}
				foreach (var classGroup in requiresBlock.Elements("ClassGroup"))
				{
					if (character.ClassGroups.Contains(classGroup.GetAttribute("Name")))
						hasCount += 1;
				}
				if (hasCount < minCount)
					return false;
			}
			return true;
		}

		public string BookName
		{
			get { return m_BookDefinition.GetAttribute("Name"); }
		}

		[CalculatedField("BookName")]
		public string BookReference
		{
			get { return BookName; }
		}

		internal void ResolvedFreeSkills(Catalog catalog)
		{
			foreach (var skill in m_SkillDefintion.Elements("FreeSkill"))
			{
				var skillEntry = catalog.Skills.FindByName(skill.GetAttribute("Name"));

				var overrideBaseScore = skill.GetInt2("BaseScore");
				var overrideSecondaryScore = skill.GetInt2("SecondaryScore");
				var overridePerLevel = skill.GetInt2("PerLevel");
				var level = skill.GetInt2("Level");

				if (skillEntry == null)
					throw new Exception(String.Format("Cannot find free skill named '{0}' for skill '{1}' in file {2}", skill.GetAttribute("Name"), Name, BookName));
				else
					FreeSkills.Add(new SkillDefinition(GroupName, skillEntry, overrideBaseScore, overrideSecondaryScore, overridePerLevel, level));
			}
		}
	}
}
